/*
 * Copyright (c) 2010, VeRSI Consortium
 *   (Victorian eResearch Strategic Initiative, Australia)
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the VeRSI, the VeRSI Consortium members, nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package versi.vitro.oaiserviceprovider.service.handle.helper;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.MalformedURLException;
import java.net.URL;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSession;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * Service implementing {@link HandleHelper}.
 */
public class HandleHelperImpl implements HandleHelper {

    /** log4j instance. */
    private static final Logger LOGGER = Logger.getLogger(HandleHelperImpl.class);

    /** ANDS service URL. */
    private String andServiceURL;

    /** ANDS Service App Id. */
    private String serviceAppId;

    /** ANDS service identifier. */
    private String identifier;

    /** ANDS service authDomain. */
    private String authDomain;

    /** hostname verification. */
    private HostnameVerifier hv;

    /**
     * Default Constructor.
     */
    public HandleHelperImpl() {
	super();
    }

    /**
     * Default Constructor.
     * 
     * @param andServiceURL
     * @param serviceAppId
     * @param identifier
     * @param authDomain
     * 
     */
    public HandleHelperImpl(final String andServiceURL, final String serviceAppId, final String identifier,
	    final String authDomain) {
	this.andServiceURL = andServiceURL;
	this.serviceAppId = serviceAppId;
	this.identifier = identifier;
	this.authDomain = authDomain;
    }

    /** {@inheritDoc} */
    public void setServiceAppId(final String serviceAppId) {
	this.serviceAppId = serviceAppId;
    }

    /** {@inheritDoc} */
    public void setAndServiceURL(final String andServiceURL) {
	this.andServiceURL = andServiceURL;
    }

    /** {@inheritDoc} */
    public void setIdentifier(final String identifier) {
	this.identifier = identifier;
    }

    /** {@inheritDoc} */
    public void setAuthDomain(final String authDomain) {
	this.authDomain = authDomain;
    }

    /** {@inheritDoc} */
    public String pidCreateHandle(final String resourceType, final String resourceValue, final int index) {

	try {

	    if (resourceType == null) {

		throw new IllegalArgumentException("ResourceType should not be null");

	    }
	    if (resourceValue == null) {

		throw new IllegalArgumentException("resourceValue should not be null");

	    }
	    if (index == 0) {

		throw new IllegalArgumentException("resourceValue should not be zero");

	    }

	    establishANDScommunication();

	    String params = "<request name=\"mint\">" + "    <properties>"
		    + "        <property name=\"authType\" value=\"SSLHost\"/>"
		    + "        <property name=\"identifier\" value=\"" + identifier + "\" />"
		    + "        <property name=\"authDomain\" value=\"" + authDomain + "\" />"
		    + "        <property name=\"appId\" value=\"" + serviceAppId + "\" />" + "    </properties>"
		    + "</request>";

	    URL url = null;

	    if (resourceType.equalsIgnoreCase("url")) {

		url = new URL(andServiceURL + "/mint?type=URL&value=" + resourceValue + "&index=" + index);

	    } else if (resourceType.equalsIgnoreCase("desc")) {

		url = new URL(andServiceURL + "/mint?type=DESC&value=" + resourceValue + "&index=" + index);

	    } else {

		throw new IllegalArgumentException("ResourceType should be either URL or DESC");

	    }

	    String response = sendCommand(url, params);
	    if (response != null) {
		return extractHandleFromResponse(response);

	    }
	} catch (RuntimeException re) {

	    LOGGER.error("Unable to create a handle", re);
	    throw re;
	} catch (Exception e) {

	    LOGGER.error("Unable to create a handle", e);
	    throw new RuntimeException("Unable to create a handle", e);

	}

	return null;
    }

    /** {@inheritDoc} */
    public String pidCreateHandle(final String resourceType, final String resourceValue) {
	try {

	    if (resourceType == null) {

		throw new IllegalArgumentException("ResourceType should not be null");

	    }
	    if (resourceValue == null) {

		throw new IllegalArgumentException("resourceValue should not be null");

	    }
	    establishANDScommunication();

	    String params = "<request name=\"mint\">" + "    <properties>"
		    + "        <property name=\"authType\" value=\"SSLHost\"/>"
		    + "        <property name=\"identifier\" value=\"" + identifier + "\" />"
		    + "        <property name=\"authDomain\" value=\"" + authDomain + "\" />"
		    + "        <property name=\"appId\" value=\"" + serviceAppId + "\" />" + "    </properties>"
		    + "</request>";

	    URL url = null;
	    if (resourceValue != null) {

		if (resourceType.equalsIgnoreCase("url")) {

		    url = new URL(andServiceURL + "/mint?type=URL&value=" + resourceValue);

		} else if (resourceType.equalsIgnoreCase("desc")) {

		    url = new URL(andServiceURL + "/mint?type=DESC&value=" + resourceValue);

		} else {
		    throw new IllegalArgumentException("ResourceType should be either URL or DESC");

		}

	    } else {
		throw new IllegalArgumentException("ResourceValue should not be null");
	    }

	    String response = sendCommand(url, params);
	    if (response != null) {
		return extractHandleFromResponse(response);

	    }
	} catch (RuntimeException re) {

	    LOGGER.error("Unable to create a handle", re);
	    throw re;
	} catch (Exception e) {

	    LOGGER.error("Unable to create a handle", e);
	    throw new RuntimeException("Unable to create a handle", e);

	}
	return null;
    }

    /** {@inheritDoc} */
    public String pidAddValue(final String handle, final String resourceType, final String resourceValue) {
	try {

	    if (handle == null) {

		throw new IllegalArgumentException("handle value should not be null");

	    }
	    if (resourceValue == null) {

		throw new IllegalArgumentException("resourceValue should not be null");

	    }
	    if (resourceType == null) {

		throw new IllegalArgumentException("resourceType should not be null");

	    }
	    establishANDScommunication();

	    String params = "<request name=\"addValue\">" + "    <properties>"
		    + "        <property name=\"authType\" value=\"SSLHost\"/>"
		    + "        <property name=\"identifier\" value=\"" + identifier + "\" />"
		    + "        <property name=\"authDomain\" value=\"" + authDomain + "\" />"
		    + "        <property name=\"appId\" value=\"" + serviceAppId + "\" />" + "    </properties>"
		    + "</request>";

	    URL url = null;
	    if (resourceValue != null) {

		if (resourceType.equalsIgnoreCase("url")) {

		    url = new URL(andServiceURL + "/addValue?type=URL&value=" + resourceValue + "&handle=" + handle);

		} else if (resourceType.equalsIgnoreCase("desc")) {

		    url = new URL(andServiceURL + "/addValue?type=DESC&value=" + resourceValue + "&handle=" + handle);

		} else {
		    throw new IllegalArgumentException("ResourceType should be either URL or DESC");

		}

	    } else {
		throw new IllegalArgumentException("ResourceValue should not be null");
	    }
	    String response = sendCommand(url, params);
	    if (response != null) {
		return extractHandleFromResponse(response);

	    }
	} catch (RuntimeException re) {

	    LOGGER.error("Unable to add value to a handle", re);
	    throw re;
	} catch (Exception e) {

	    LOGGER.error("Unable to add value to a handle", e);
	    throw new RuntimeException("Unable to add value to a handle", e);

	}
	return null;
    }

    /** {@inheritDoc} */
    public String pidAddValue(final String handle, final String resourceType, final String resourceValue,
	    final int index) {

	try {

	    if (handle == null) {

		throw new IllegalArgumentException("handle value should not be null");

	    }
	    if (resourceValue == null) {

		throw new IllegalArgumentException("resourceValue should not be null");

	    }
	    if (resourceType == null) {

		throw new IllegalArgumentException("resourceType should not be null");

	    }
	    if (index == 0) {

		throw new IllegalArgumentException("index should not be zero");

	    }

	    establishANDScommunication();

	    String params = "<request name=\"addValueByIndex\">" + "    <properties>"
		    + "        <property name=\"authType\" value=\"SSLHost\"/>"
		    + "        <property name=\"identifier\" value=\"" + identifier + "\" />"
		    + "        <property name=\"authDomain\" value=\"" + authDomain + "\" />"
		    + "        <property name=\"appId\" value=\"" + serviceAppId + "\" />" + "    </properties>"
		    + "</request>";

	    URL url = null;
	    if (resourceValue != null) {

		if (resourceType.equalsIgnoreCase("url")) {

		    url = new URL(andServiceURL + "/addValueByIndex?type=URL&value=" + resourceValue + "&index="
			    + index + "&handle=" + handle);

		} else if (resourceType.equalsIgnoreCase("desc")) {

		    url = new URL(andServiceURL + "/addValueByIndex?type=DESC&value=" + resourceValue + "&index="
			    + index + "&handle=" + handle);

		} else {
		    throw new IllegalArgumentException("ResourceType should be either URL or DESC");

		}

	    } else {
		throw new IllegalArgumentException("ResourceValue should not be null");
	    }

	    String response = sendCommand(url, params);
	    if (response != null) {
		return extractHandleFromResponse(response);

	    }
	} catch (RuntimeException re) {

	    LOGGER.error("Unable to add value to a handle", re);
	    throw re;
	} catch (Exception e) {

	    LOGGER.error("Unable to add value to a handle", e);
	    throw new RuntimeException("Unable to add value to a handle", e);

	}

	return null;

    }

    /** {@inheritDoc} */
    public boolean pidModifyHandle(final String handle, final int index, final String resourceValue) {

	try {

	    if (handle == null) {

		throw new IllegalArgumentException("handle value should not be null");

	    }
	    if (resourceValue == null) {

		throw new IllegalArgumentException("resourceValue should not be null");

	    }
	    if (index == 0) {

		throw new IllegalArgumentException("index should not be zero");

	    }

	    establishANDScommunication();

	    String params = "<request name=\"mint\">" + "    <properties>"
		    + "        <property name=\"authType\" value=\"SSLHost\"/>"
		    + "        <property name=\"identifier\" value=\"" + identifier + "\" />"
		    + "        <property name=\"authDomain\" value=\"" + authDomain + "\" />"
		    + "        <property name=\"appId\" value=\"" + serviceAppId + "\" />" + "    </properties>"
		    + "</request>";
	    URL url = null;
	    if (resourceValue != null) {

		url = new URL(andServiceURL + "/modifyValueByIndex?handle=" + handle + "&index=" + index + "&value="
			+ resourceValue);

	    } else {
		throw new IllegalArgumentException("ResourceValue should not be null");
	    }

	    String response = sendCommand(url, params);
	    if (response != null) {
		// return extractHandleFromResponse(response);
		return true;
	    }
	} catch (RuntimeException re) {

	    LOGGER.error("Unable to modify a handle value", re);
	    throw re;
	} catch (Exception e) {

	    LOGGER.error("Unable to modify a handle value", e);
	    throw new RuntimeException("Unable to modify a handle value", e);

	}

	return false;
    }

    /** {@inheritDoc} */
    public boolean pidDeleteValueByIndex(final String handle, final int index) {

	try {

	    if (handle == null) {

		throw new IllegalArgumentException("handle value should not be null");

	    }
	    if (index == 0) {

		throw new IllegalArgumentException("index should not be zero");

	    }

	    establishANDScommunication();

	    String params = "<request name=\"deleteValueByIndex\">" + "    <properties>"
		    + "        <property name=\"authType\" value=\"SSLHost\"/>"
		    + "        <property name=\"identifier\" value=\"" + identifier + "\" />"
		    + "        <property name=\"authDomain\" value=\"" + authDomain + "\" />"
		    + "        <property name=\"appId\" value=\"" + serviceAppId + "\" />" + "    </properties>"
		    + "</request>";

	    URL url = null;
	    if (handle != null) {
		url = new URL(andServiceURL + "/deleteValueByIndex?handle=" + handle + "&index=" + index);
	    } else {
		throw new IllegalArgumentException("Handle Value should not be null");
	    }
	    String response = sendCommand(url, params);
	    if (response != null) {

		// Successfully deleted handle value
		// return extractHandleFromResponse(response);
		LOGGER.debug("Successfully deleted handle value");
		return true;
	    }
	} catch (Exception e) {

	    LOGGER.error("Unable deleted handle value", e);
	}

	return false;

    }

    /** {@inheritDoc} */
    public String[] pidListHandle() {

	establishANDScommunication();
	try {

	    String params = "<request name=\"listHandles\">" + "    <properties>"
		    + "        <property name=\"authType\" value=\"SSLHost\"/>"
		    + "        <property name=\"identifier\" value=\"" + identifier + "\" />"
		    + "        <property name=\"authDomain\" value=\"" + authDomain + "\" />"
		    + "        <property name=\"appId\" value=\"" + serviceAppId + "\" />" + "    </properties>"
		    + "</request>";
	    URL url;

	    url = new URL(andServiceURL + "/listHandles");

	    String response = sendCommand(url, params);
	    if (response != null) {
		// return extractHandleFromResponse(response);
		return null;
	    }
	} catch (MalformedURLException e) {
	    // TODO Auto-generated catch block
	    e.printStackTrace();
	}
	return null;
    }

    protected void establishANDScommunication() {

	try {

	    hv = new HostnameVerifier();

	    trustAllHttpsCertificates();

	} catch (Exception e) {

	    LOGGER.error("Cannot establish ANDS handle server connection: " + e);
	}

    }

    /**
     * Sends parameters to ANDS service.
     * */
    protected String sendCommand(final URL serviceURL, final String params) {
	String response = "";
	int responseCode = 0;
	try {

	    HttpsURLConnection.setDefaultHostnameVerifier(hv);
	    HttpsURLConnection conn = null;

	    conn = (HttpsURLConnection) serviceURL.openConnection();

	    conn.setConnectTimeout(10000);
	    conn.setRequestMethod("POST");
	    conn.setAllowUserInteraction(false);
	    conn.setDoOutput(true);
	    conn.setRequestProperty("User-Agent", "PIDExample/1.0");
	    conn.setRequestProperty("Content-Type", "text/xml");
	    conn.setRequestProperty("Content-Encoding", "UTF-8");
	    conn.setRequestProperty("Content-Length", Integer.toString(params.length()));
	    OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream());
	    out.write(params);
	    out.flush();
	    out.close();
	    responseCode = conn.getResponseCode();

	    BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
	    String temp;

	    while ((temp = in.readLine()) != null) {
		response += temp + "\n";
	    }
	    temp = null;
	    in.close();
	    LOGGER.debug("Server response:\n" + response);

	    LOGGER.debug("Server code:\n" + responseCode);
	    conn.disconnect();
	} catch (IOException e) {
	    // TODO Auto-generated catch block
	    e.printStackTrace();
	}

	if (responseCode == 200) // 200 means OK
	{
	    LOGGER.debug("Successfully handle operation");
	    return response;
	}
	return null;

    }

    /**
     * Gets an attribute value from an XML node.
     * */
    private static String getAttributeValue(Node node, String attrName) {
	NamedNodeMap nmm = node.getAttributes();
	for (int j = 0; j < nmm.getLength(); j++) {
	    Node attribute = nmm.item(j);
	    if (attribute.getNodeType() != Node.ATTRIBUTE_NODE) {
		continue;
	    }
	    String nodeName = attribute.getNodeName();
	    if (nodeName.equals(attrName)) {
		return attribute.getNodeValue();
	    }
	}
	return null;
    }

    // private static String extractHandleFromResponse(final String response)

    /** {@inheritDoc} */
    public String extractHandleFromResponse(final String response) {

	String handleAttr = null;
	try {

	    // Document doc = null;

	    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();

	    DocumentBuilder builder = factory.newDocumentBuilder();

	    // doc = builder.parse(new InputSource(new StringReader(response)));

	    Document doc = builder.parse(new ByteArrayInputStream(response.getBytes()));

	    // get root element of the template
	    Element rootElement = doc.getDocumentElement();
	    NodeList nodeList = rootElement.getElementsByTagName("identifier");
	    if (null != nodeList && nodeList.getLength() > 0) {
		handleAttr = getAttributeValue(nodeList.item(0), "handle");
		// getAttributes().getNamedItem("handle").get;

	    } else {

		throw new RuntimeException("");
	    }

	    /*
	     * NodeList nodeList = doc.getElementsByTagName("response"); Element
	     * responseNode = (Element) (nodeList.item(0)); NodeList nodeList2 =
	     * responseNode.getElementsByTagName("identifier"); Element
	     * identifierNode = (Element) nodeList2.item(1); String handleAttr =
	     * identifierNode.getAttribute("handle");
	     */

	    return handleAttr;

	} catch (ParserConfigurationException e) {
	    LOGGER.error("Unable extract handle value from response message.");
	} catch (SAXException e) {
	    LOGGER.error("Unable extract handle value from response message.");
	} catch (IOException e) {
	    // TODO Auto-generated catch block
	    LOGGER.error("Unable extract handle value from response message.");

	}

	return null;

    }

    /**
     * All trust certificates.
     * 
     * @exception java.lang.Exception.
     * */
    private static void trustAllHttpsCertificates() throws Exception {

	// Create a trust manager that does not validate certificate chains:

	javax.net.ssl.TrustManager[] trustAllCerts =

	new javax.net.ssl.TrustManager[1];

	javax.net.ssl.TrustManager tm = new miTM();

	trustAllCerts[0] = tm;

	javax.net.ssl.SSLContext sc =

	javax.net.ssl.SSLContext.getInstance("SSL");

	sc.init(null, trustAllCerts, null);

	javax.net.ssl.HttpsURLConnection.setDefaultSSLSocketFactory(

	sc.getSocketFactory());

    }

    /**
     * use X509 certificates to authenticate the remote side of a secure socket.
     */
    public static class miTM implements javax.net.ssl.TrustManager, javax.net.ssl.X509TrustManager {

	/** overwritten method, always return null. */
	public java.security.cert.X509Certificate[] getAcceptedIssuers() {
	    return null;
	}

	/** overwritten method, always return true. */
	public boolean isServerTrusted(java.security.cert.X509Certificate[] certs) {
	    return true;
	}

	/** overwritten method, always return true. */
	public boolean isClientTrusted(java.security.cert.X509Certificate[] certs) {
	    return true;
	}

	/**
	 * return if it can be validated and is trusted for server SSL
	 * authentication based on the authentication type.
	 */
	public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType)
		throws java.security.cert.CertificateException {
	    return;
	}

	/**
	 * return if it can be validated and is trusted for client SSL
	 * authentication based on the authentication type.
	 */
	public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType)
		throws java.security.cert.CertificateException {
	    return;
	}
    }

    /**
     * During handshaking, if the URL's hostname and the server's identification
     * hostname mismatch, the verification mechanism can call back to
     * implementers of this interface to determine if this connection should be
     * allowed.
     */
    private static class HostnameVerifier implements javax.net.ssl.HostnameVerifier {
	public boolean verify(final String urlHostName, final SSLSession session) {
	    LOGGER.debug("Warning: URL Host: " + urlHostName + " vs. " + session.getPeerHost());
	    return true;
	}
    }

}
